﻿/*
 * TrackerReporter
 * 
 * Author: snowdreamist
 * Date: 2011/11/20
 * 
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using BitTorrentSharp.Torrent;

namespace BitTorrentSharp.Tracker
{
    /// <summary>
    /// tracker reporter
    /// </summary>
    public class TrackerReporter
    {
        #region consts
        /// <summary>
        /// default retry times
        /// </summary>
        internal const int DefaultRetryTimes = 5;
        /// <summary>
        /// Peers number wanted 
        /// </summary>
        internal const int DefaultPeerNumberWanted = 50;

        #endregion

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="peerID"></param>
        /// <param name="getDownloaded"></param>
        /// <param name="getUploaded"></param>
        /// <param name="getLeft"></param>
        /// <param name="getEvent"></param>
        /// <param name="enableCompract"></param>
        public TrackerReporter(
            string peerID,
            Uri announceUri,
            byte[] infoHash,
            Func<long> getDownloaded,
            Func<long> getUploaded,
            Func<long> getLeft,
            Func<TrackerEvent> getEvent,
            bool enableCompract)
        {
            if (peerID == null)
                throw new ArgumentNullException("peerID");
            if (!PeerIDUtility.VerifyPeerID(peerID))
                throw new ArgumentException("Invalid peerID");
            this.PeerID = peerID;

            if (announceUri == null)
                throw new ArgumentNullException("announceUri");
            this.AnnounceUri = announceUri;

            if (infoHash == null)
                throw new ArgumentNullException("infoHash");
            this._infoHash = infoHash;

            if (getDownloaded == null)
                throw new ArgumentNullException("getDownloaded");
            this._getDownloaded = getDownloaded;

            if (getUploaded == null)
                throw new ArgumentNullException("getUploaded");
            this._getUploaded = getUploaded;

            if (getLeft == null)
                throw new ArgumentNullException("getLeft");
            this._getLeft = getLeft;
            
            this._getEvent = getEvent;
            this.EnableCompract = enableCompract;
            // disable timer
            this._reportTimer = new Timer(this.ReportTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
            this.ReportInterval = TimeSpan.MaxValue;
            this.MinReportInterval = TimeSpan.Zero;
            this._lastReportTime = DateTime.MinValue;
        }

        #region private member
        /// <summary>
        /// Report sync lock object
        /// </summary>
        private readonly object _syncReport = new object();
        /*
         * State achieve functions
         */
        private Func<long> _getDownloaded;
        private Func<long> _getUploaded;
        private Func<long> _getLeft;
        private Func<TrackerEvent> _getEvent;
        /// <summary>
        /// info hash
        /// </summary>
        private byte[] _infoHash;
        /// <summary>
        /// Report timer
        /// </summary>
        private Timer _reportTimer;
        /// <summary>
        /// trackerID if exists
        /// </summary>
        private byte[] _trackerID;
        /// <summary>
        /// try times
        /// </summary>
        private int _retryTimes = TrackerReporter.DefaultRetryTimes;
        /// <summary>
        /// Peer number wanted
        /// </summary>
        private int _peerNumberWanted = TrackerReporter.DefaultPeerNumberWanted;
        /// <summary>
        /// last report time
        /// </summary>
        private DateTime _lastReportTime;

        #endregion

        #region properties
        /// <summary>
        /// Announce uri
        /// </summary>
        public Uri AnnounceUri { get; private set; }
        /// <summary>
        /// peer id
        /// </summary>
        public string PeerID { get; private set; }
        /// <summary>
        /// enable compression or not
        /// </summary>
        public bool EnableCompract { get; private set; }
        /// <summary>
        /// Ip or dnc, null is acceptable
        /// </summary>
        public string IPOrDNS { get; set; }
        /// <summary>
        /// port
        /// </summary>
        public int Port { get; set; }
        /// <summary>
        /// Report interval
        /// </summary>
        public TimeSpan ReportInterval { get; private set; }
        /// <summary>
        /// Min report interval
        /// </summary>
        public TimeSpan MinReportInterval { get; private set; }
        /// <summary>
        /// Retry send request times
        /// </summary>
        public int RetryTimes
        {
            get
            {
                return this._retryTimes;
            }
            set
            {
                if (value < 1)
                    throw new ArgumentOutOfRangeException("RetryTimes cannot be smaller than 1");
                this._retryTimes = value;
            }
        }
        /// <summary>
        /// Peer number wanted
        /// </summary>
        public int PeerNumberWanted
        {
            get
            {
                return this._peerNumberWanted;
            }
            set
            {
                if (value < 1)
                    throw new ArgumentOutOfRangeException("PeerNumberWanted cannot smalled than 1");
                this._peerNumberWanted = value;
            }
        }
        /// <summary>
        /// Client key used to unique identity client [Optional]
        /// null acceptable
        /// </summary>
        public byte[] ClientKey { get; set; }
        /// <summary>
        /// Can report now
        /// </summary>
        public bool CanReport
        {
            get
            {
                return DateTime.Now - this._lastReportTime > this.MinReportInterval;
            }
        }

        #endregion

        /// <summary>
        /// Report to tracker
        /// </summary>
        /// <param name="trackerEvent">current event, if set then, getEventFunc will be ignored</param>
        public void Report(Nullable<TrackerEvent> trackerEvent)
        {
            this.InternalReport(trackerEvent);
        }

        #region events
        /// <summary>
        /// Before report
        /// </summary>
        public event EventHandler<ReportingEventArgs> Reporting;
        /// <summary>
        /// After report
        /// </summary>
        public event EventHandler<ReportedEventArgs> Reported;

        #endregion

        #region protected
        /// <summary>
        /// Before report
        /// </summary>
        protected virtual void OnReporting()
        {
            if (this.Reporting != null)
                this.Reporting(this, new ReportingEventArgs());
        }
        /// <summary>
        /// After report
        /// </summary>
        /// <param name="isSucceed"></param>
        protected virtual void OnReported(Exception error, TrackerWebResponse response)
        {
            if (this.Reported != null)
                this.Reported(this, new ReportedEventArgs(error, response));
        }

        #endregion

        #region privare

        private void InternalReport(Nullable<TrackerEvent> trackerEvent)
        {
            // check min intervel
            if (!this.CanReport)
                throw new InvalidOperationException("Report is too frequent");

            // try enter, no wait
            if (Monitor.TryEnter(this._syncReport, 0))
            {
                Exception _innerError = null;
                TrackerWebResponse response = null;

                // enter
                try
                {
                    // report

                    TrackerWebRequest request = new TrackerWebRequest();
                    // build request
                    request.PeerID = this.PeerID;
                    request.EnableCompact = this.EnableCompract;
                    request.AnnountUri = this.AnnounceUri;
                    request.InfoHash = this._infoHash;
                    request.Downloaded = this._getDownloaded();
                    request.Uploaded = this._getUploaded();
                    request.Left = this._getLeft();
                    request.Event = trackerEvent.HasValue
                                    ?
                                    trackerEvent.Value
                                    :
                                    this._getEvent != null
                                    ?
                                    this._getEvent()
                                    :
                                    (Nullable<TrackerEvent>)null;

                    request.IPOrDNS = string.IsNullOrWhiteSpace(this.IPOrDNS) ? null : this.IPOrDNS;
                    request.Port = this.Port;
                    request.PeersCountWanted = this.PeerNumberWanted;
                    request.Key = this.ClientKey;
                    request.TrackerID = this._trackerID;
                    
                    // try get response
                    // NOTE no matter succeed or not, record this?
                    this._lastReportTime = DateTime.Now;

                    if (this.TryGetResponse(request, out response))
                    {
                        // succeed
                        this.HandleResponse(response);
                    }
                    else
                    {
                        // failed
                        throw new InvalidOperationException("Cannot get response from tracker");
                    }
                }
                catch (Exception error)
                {
                    _innerError = error;
                }
                finally
                {
                    Monitor.Exit(this._syncReport);
                }

                // event
                this.OnReported(_innerError, response);
            }
            else
            {
                // no
                throw new InvalidOperationException("Another reporting is being processed");
            }

        }

        private bool TryGetResponse(TrackerWebRequest request, out TrackerWebResponse response)
        {
            int tryTime = 0;

            response = default(TrackerWebResponse);

            while (tryTime <= this.RetryTimes)
            {
                bool isSucceed = false;

                try
                {
                    response = request.GetResponse();
                    isSucceed = true;
                }
                catch(Exception error)
                {
                    System.Diagnostics.Debug.WriteLine(string.Format("TrackerReporter.TryGetResponse failed, exception:\n{0}", error), "TrackerReporter");
                    isSucceed = false;
                }

                if (isSucceed)
                    return true;

                tryTime++;
            }

            return false;
        }

        private void HandleResponse(TrackerWebResponse response)
        {
            // update interval
            if (response.Interval != this.ReportInterval)
            {
                this.ReportInterval = response.Interval;
                this._reportTimer.Change(this.ReportInterval, this.ReportInterval);
            }
            // update mininterval
            this.MinReportInterval = response.MinInterval;
            // update trackerID
            this._trackerID = response.TackerID;
        }

        private void ReportTimerCallback(object state)
        {
            if (this.CanReport)
            {
                try
                {
                    this.Report(null);
                }
                catch (Exception error)
                {
                    System.Diagnostics.Debug.WriteLine(string.Format("TrackerReporter.ReportTimerCallback failed, exception:\n{0}", error), "TrackerReporter");
                }
            }
        }

        #endregion
    }
}
